Équipe 01 : ALLOUCH Bilal (Chef d’équipe) / COLSON Paul / LECOQ Aliça / LINARD Raphaël
L’Agence Internationale pour la Vigilance Météorologique (AIVM) lance un programme international pour équiper les navires de stations météo embarquées capables de mesurer les paramètres influençant la formation de cyclones et autres catastrophes naturelles.
Notre startup, OceanTech Systems, a été choisie pour concevoir le prototype de cette station.
L’objectif est de créer un système simple, fiable et autonome, pouvant être piloté par n’importe quel membre d’équipage, sans connaissances techniques.
Cette station permet de mesurer la température, l’humidité, la pression et la luminosité, d’enregistrer les données sur carte SD avec date et heure, et d’indiquer l’état du système par LED.
Elle contribue à la prévention des risques météorologiques en mer grâce à une surveillance continue et automatisée.
Destinée aux utilisateurs non techniciens (membres d’équipage, opérateurs).
Cette partie explique comment utiliser la station météo, à quoi servent les composants, comment la faire fonctionner, et comment interpréter les voyants et les données enregistrées.
Dans cette section, vous trouverez les photos et descriptions détaillées de chaque élément utilisé dans la station météo.




Cette vidéo présente étape par étape le montage complet de la station météo embarquée réalisée sur Arduino UNO.
Elle montre comment connecter correctement chaque module au shield Grove et à la carte SD, afin d’obtenir un système fonctionnel prêt à exécuter le programme de la station.
Ordre des connexions dans la vidéo :
1 Installation de la carte Arduino UNO sur la table de montage.
2 Ajout de la carte Grove Base Shield sur l’Arduino (interface principale).
3 Branchement du module double bouton (rouge/vert) sur le port D2 du shield.
4 Connexion du capteur de luminosité sur l’entrée A0 du shield.
5 Raccordement de la LED RGB chaînable sur la sortie D6.
6 Connexion du capteur BME280 sur le bus I²C du shield (SDA/SCL).
7 Insertion du module carte SD Shield sur l’Arduino, puis de la carte microSD dans le lecteur.
8 Branchement du module RTC DS1307 sur le port I²C du SD Shield.
Ce câblage assure une communication correcte entre tous les modules :
Cette partie t’explique simplement comment utiliser la station, changer les modes, et comprendre les couleurs de la LED.
Tu n’as pas besoin de savoir programmer : il suffit de suivre les étapes.
| Étape | Action | Résultat |
|---|---|---|
| 1 | Branche la station météo à son alimentation 5V (USB ou batterie). | — |
| 2 | La LED s’allume verte fixe 🟢 | La station démarre correctement en mode Standard. |
| 3 | La station commence à mesurer automatiquement : température, humidité, pression atmosphérique, et luminosité ambiante. | — |
| 4 | Toutes les mesures sont enregistrées sur la carte microSD, avec la date et l’heure. | Les fichiers contiennent une ligne par mesure, lisible sur un ordinateur. |
En cas de coupure d’alimentation, la station redémarre automatiquement en mode Standard.
La station dispose de 4 modes principaux.
Tu peux changer de mode grâce aux deux boutons (un vert et un rouge) selon la durée d’appui indiquée.
| Mode | Comment y accéder | Durée d’appui | LED | Description |
|---|---|---|---|---|
| 🟢 Mode Standard | Démarrage normal (aucun bouton pressé) | – | 🟢 Vert fixe | Mode principal : la station fait des mesures toutes les 10 s et les enregistre sur la carte SD. |
| 🔵 Mode Économique | Depuis le mode Standard → appui long sur le bouton vert | ≈ 5 s | 🔵 Bleu fixe | Mesures toutes les 20 s pour économiser la batterie. Pour revenir au mode Standard, fais un appui long sur le bouton vert. |
| 🟠 Mode Maintenance | Depuis le mode Standard ou Éco → appui long sur le bouton rouge | ≈ 5 s | 🟠 Orange fixe | Permet de retirer la carte SD en sécurité et de consulter les mesures depuis un PC. Appui long sur le bouton rouge pour quitter. |
| 🟡 Mode Configuration | Maintiens le bouton rouge pendant la mise sous tension | – | 🟡 Jaune fixe | Configure les paramètres (intervalle, taille de fichier, date/heure…). Si aucun réglage n’est fait, retour automatique au mode Standard. |
Chaque mode est clairement visible grâce à la couleur fixe de la LED.
La LED indique l’état du système ou une erreur éventuelle.
| Couleur | Signification |
|---|---|
| 🟢 Vert fixe | Mode Standard actif (mesure normale). |
| 🔵 Bleu fixe | Mode Économique (mesures plus espacées). |
| 🟠 Orange fixe | Mode Maintenance (sécurisation ou lecture SD). |
| 🟡 Jaune fixe | Mode Configuration (réglages depuis PC). |
| Couleur alternée | Signification du problème | Ce qu’il faut faire |
|---|---|---|
| 🔴↔🔵 Rouge / Bleu | Erreur d’accès à l’horloge RTC (heure non disponible). | Vérifie la pile du module RTC ou son branchement. |
| 🔴↔🟢 Rouge / Vert | Erreur d’accès à un capteur (ex : capteur BME280 déconnecté). | Vérifie le câble du capteur. |
| 🔴 court / 🟢 long | Données incohérentes reçues d’un capteur. | Vérifie la propreté du capteur ou les conditions autour. |
| 🔴↔⚪ Rouge / Blanc | Carte SD pleine. | Remplace ou vide la carte microSD. |
| 🔴 court / ⚪ long | Erreur d’accès ou d’écriture sur la carte SD. | Vérifie que la carte SD est bien insérée ou reformate-la en FAT32. |
La LED clignote toujours en rouge avec une autre couleur pour signaler une anomalie.
| Action | Bouton / Durée | Résultat |
|---|---|---|
| Allumer la station | – | Démarrage en 🟢 Mode Standard |
| Passer en Mode Éco | 🟢 Bouton vert – appui long (5 s) | 🔵 Mode Économique |
| Revenir au Mode Standard | 🟢 Bouton vert – appui long (5 s) | 🟢 Mode Standard |
| Passer en Maintenance | 🔴 Bouton rouge – appui long (5 s) | 🟠 Mode Maintenance |
| Quitter Maintenance | 🔴 Bouton rouge – appui long (5 s) | Retour au mode précédent |
| Démarrer en Configuration | 🔴 Bouton rouge maintenu pendant la mise sous tension | 🟡 Mode Configuration |
| Inactivité prolongée | – | Retour automatique au Mode Standard |

#define PRETTY_UI 1 // ← passe à 0 si la taille dépasse la mémoire UNO
#include <Wire.h> // i2c ---> Capteur d'humidité, température
#include <SPI.h>
#include <SD.h>
#include <EEPROM.h>
#include <RTClib.h>
#include <Adafruit_BME280.h>
#include <ChainableLED.h>
/* ---------- Pins ---------- */
#define PIN_LED_DATA 6
#define PIN_LED_CLK 7
#define PIN_BTN_V 3 // Vert
#define PIN_BTN_R 2 // Rouge
#define PIN_LUM A0
#define PIN_SD_CS 4
/* ---------- Types ---------- */
enum LedColor {
LED_OFF,
LED_G,
LED_B,
LED_Y,
LED_O,
LED_R,
LED_W
};
enum Mode {
STANDARD,
ECONOMIQUE,
MAINTENANCE,
CONFIGURATION
};
/* ---------- Config & capteurs ---------- */
struct Settings {
uint16_t logMin;
uint16_t toutSec;
uint32_t maxBytes;
uint8_t enMask;
uint16_t crc;
};
struct Sensor;
/* ---------- Prototypes ---------- */
uint16_t crc16(const Settings &s);
void setDefaults(void);
void saveEEPROM(void);
void loadEEPROM(void);
void setLed(enum LedColor c);
void isrV(void);
void isrR(void);
bool initBME(struct Sensor *s);
void readBME(struct Sensor *s, float *t, float *h, float *p);
bool initLUM(struct Sensor *s);
void readLUM(struct Sensor *s, float *t, float *h, float *p);
void logOnce(void);
void enterMode(enum Mode m);
void tickStd(void);
void tickEco(void);
void tickMaint(void);
void tickCfg(void);
bool openNewLog(void);
void rotateIfNeed(void);
void handleCmdChar(char c);
void handleCmdLine(void);
void printDateTime(Print &out);
void printNum(Print &out, float v, uint8_t prec);
void labelVal(
Print &out,
const __FlashStringHelper *label,
float v,
uint8_t prec,
const __FlashStringHelper *unit
);
void showHelp(void);
/* ---------- Alertes LED ---------- */
enum ErrFlags {
ERR_NONE = 0,
ERR_RTC = 1 << 0,
ERR_SENSOR = 1 << 2,
ERR_INCOH = 1 << 3,
ERR_SD_FULL= 1 << 4,
ERR_SD_RW = 1 << 5
};
volatile uint8_t gErrors = ERR_NONE;
void updateErrorLED(void);
/* ---------- Globals ---------- */
ChainableLED led(PIN_LED_DATA, PIN_LED_CLK, 1);
RTC_DS1307 rtc;
Adafruit_BME280 bme;
volatile bool fEco = false;
volatile bool fMaint = false;
volatile bool fStd = false;
volatile bool fQuitM = false;
enum Mode modeCur = STANDARD;
enum Mode modePrev = STANDARD;
const unsigned long HOLD_MS = 2000;
volatile unsigned long tV = 0;
volatile unsigned long tR = 0;
bool haveRTC = false;
bool haveSD = false;
bool enBME = true;
bool enLUM = true;
Settings cfg;
const uint16_t DEF_LOG_MIN = 10; // secondes
const uint16_t DEF_TOUT = 3; // secondes
const uint32_t DEF_MAXB = 2048;
File logFile;
char logName[16] = "LOG000.CSV";
uint16_t logIdx = 0;
/* ---------- Sensor struct ---------- */
struct Sensor {
const char *name;
bool *en;
bool present;
bool (*initFn)(struct Sensor *);
void (*readFn)(struct Sensor *, float *, float *, float *);
};
Sensor sBME = {
"BME",
&enBME,
false,
initBME,
readBME
};
Sensor sLUM = {
"LUM",
&enLUM,
true,
initLUM,
readLUM
};
Sensor *sInit[] = {
&sBME,
&sLUM
};
/* ---------- LED base + moteur ---------- */
LedColor baseColor = LED_W;
unsigned long ledBeatMs = 0;
bool ledPhase = false;
void setLed(enum LedColor c) {
switch (c) {
case LED_G:
led.setColorRGB(0, 0, 255, 0);
break;
case LED_B:
led.setColorRGB(0, 0, 0, 255);
break;
case LED_Y:
led.setColorRGB(0, 255, 255, 0);
break;
case LED_O:
led.setColorRGB(0, 255, 100, 0);
break;
case LED_R:
led.setColorRGB(0, 255, 0, 0);
break;
case LED_W:
led.setColorRGB(0, 255, 255, 255);
break;
default:
led.setColorRGB(0, 0, 0, 0);
break;
}
}
/* ---------- EEPROM ---------- */
uint16_t crc16(const Settings &s) {
const uint8_t *p = (const uint8_t *)&s;
uint16_t sum = 0;
for (size_t i = 0; i < sizeof(Settings) - 2; i++) {
sum += p[i];
}
return sum ^ 0xA55A;
}
void setDefaults(void) {
cfg.logMin = DEF_LOG_MIN;
cfg.toutSec = DEF_TOUT;
cfg.maxBytes = DEF_MAXB;
cfg.enMask = 0;
if (enBME) {
cfg.enMask |= 1;
}
if (enLUM) {
cfg.enMask |= 2;
}
cfg.crc = crc16(cfg);
}
void saveEEPROM(void) {
cfg.enMask = 0;
if (enBME) {
cfg.enMask |= 1;
}
if (enLUM) {
cfg.enMask |= 2;
}
cfg.crc = crc16(cfg);
EEPROM.put(0, cfg);
}
void loadEEPROM(void) {
EEPROM.get(0, cfg);
if (crc16(cfg) != cfg.crc) {
setDefaults();
saveEEPROM();
}
enBME = cfg.enMask & 1;
enLUM = cfg.enMask & 2;
}
/* ---------- ISR (boutons inversés) ---------- */
void isrV(void) {
int lv = digitalRead(PIN_BTN_V);
if (lv == LOW) {
tV = millis();
} else {
if (tV && (millis() - tV >= HOLD_MS)) {
if (modeCur == ECONOMIQUE) {
fStd = true;
} else {
fEco = true;
}
}
tV = 0;
}
}
void isrR(void) {
int lr = digitalRead(PIN_BTN_R);
if (lr == LOW) {
tR = millis();
} else {
if (tR && (millis() - tR >= HOLD_MS)) {
if (modeCur == MAINTENANCE) {
fQuitM = true;
} else {
fMaint = true;
}
}
tR = 0;
}
}
/* ---------- SD ---------- */
bool openNewLog(void) {
if (logFile) {
logFile.flush();
logFile.close();
}
snprintf(logName, sizeof(logName), "LOG%03u.CSV", logIdx++);
logFile = SD.open(logName, FILE_WRITE);
if (!logFile) {
gErrors |= ERR_SD_FULL;
#if PRETTY_UI
Serial.println(F("🟥📁 SD: ouverture fichier échouée (pleine ?"));
#else
Serial.println(F("SD open fail"));
#endif
return false;
}
gErrors &= ~ERR_SD_FULL;
if (logFile.size() == 0) {
logFile.println(F("DateTime;TempC;Hum%;hPa;Lum"));
}
#if PRETTY_UI
Serial.print(F("📄 Nouveau log: "));
Serial.println(logName);
#endif
return true;
}
void rotateIfNeed(void) {
if (logFile && (uint32_t)logFile.size() >= cfg.maxBytes) {
openNewLog();
}
}
/* ---------- Capteurs ---------- */
bool initBME(struct Sensor *s) {
bool ok = bme.begin(0x76);
if (!ok) {
ok = bme.begin(0x77);
}
s->present = ok;
#if PRETTY_UI
Serial.println(
ok ? F("✅ BME280 détecté") : F("❌ BME280 absent")
);
#else
Serial.println(
ok ? F("BME ok") : F("BME abs")
);
#endif
return ok;
}
bool initLUM(struct Sensor *s) {
pinMode(PIN_LUM, INPUT);
s->present = true;
#if PRETTY_UI
Serial.println(F("✅ Capteur luminosité prêt"));
#else
Serial.println(F("LUM ok"));
#endif
return true;
}
void readBME(struct Sensor *s, float *t, float *h, float *p) {
if (!s->present || !(*s->en)) {
*t = *h = *p = NAN;
return;
}
*t = bme.readTemperature();
*h = bme.readHumidity();
*p = bme.readPressure() / 100.0f;
}
void readLUM(struct Sensor *s, float *t, float *h, float *p) {
(void)t;
(void)h;
if (!(*s->en)) {
*p = NAN;
return;
}
*p = (float)analogRead(PIN_LUM);
}
/* ---------- Impression ---------- */
void printDateTime(Print &out) {
if (!haveRTC) {
out.print(F("NA"));
return;
}
DateTime n = rtc.now();
char b[20];
snprintf(
b,
sizeof(b),
"%04u-%02u-%02u %02u:%02u:%02u",
n.year(), n.month(), n.day(),
n.hour(), n.minute(), n.second()
);
out.print(b);
}
void printNum(Print &out, float v, uint8_t prec) {
if (isnan(v)) {
out.print(F("NA"));
return;
}
char buf[16];
dtostrf(v, 0, prec, buf);
out.print(buf);
}
void labelVal(
Print &out,
const __FlashStringHelper *label,
float v,
uint8_t prec,
const __FlashStringHelper *unit
) {
out.print(label);
out.print('=');
printNum(out, v, prec);
if (unit) {
out.print(' ');
out.print(unit);
}
}
/* ---------- Log d’une ligne ---------- */
void logOnce(void) {
float t = NAN;
float h = NAN;
float p = NAN;
float l = NAN;
sBME.readFn(&sBME, &t, &h, &p);
sLUM.readFn(&sLUM, NULL, NULL, &l);
if (enBME && !sBME.present) {
gErrors |= ERR_SENSOR;
}
// Incohérences simples (hors plage)
bool incoh = false;
if (!isnan(t) && (t < -40.0 || t > 85.0)) {
incoh = true;
}
if (!isnan(h) && (h < 0.0 || h > 100.0)) {
incoh = true;
}
if (!isnan(p) && (p < 300.0 || p > 1100.0)) {
incoh = true;
}
if (!isnan(l) && (l < 0.0 || l > 1023.0)) {
incoh = true;
}
if (incoh) {
gErrors |= ERR_INCOH;
} else {
gErrors &= ~ERR_INCOH;
}
#if PRETTY_UI
Serial.print(F("⏱ "));
printDateTime(Serial);
Serial.print(F(" | "));
labelVal(Serial, F("🌡️ Temp"), t, 1, F("°C"));
Serial.print(F(" | "));
labelVal(Serial, F("💧 Hum"), h, 0, F("%"));
Serial.print(F(" | "));
labelVal(Serial, F("🧭 Press"), p, 1, F("hPa"));
Serial.print(F(" | "));
labelVal(Serial, F("💡 Lum"), l, 0, F(""));
Serial.println();
#else
Serial.print(F("[MEAS] "));
printDateTime(Serial);
Serial.print(F(" | T="));
printNum(Serial, t, 1);
Serial.print(F("C"));
Serial.print(F(" | H="));
printNum(Serial, h, 0);
Serial.print(F("%"));
Serial.print(F(" | P="));
printNum(Serial, p, 1);
Serial.print(F("hPa"));
Serial.print(F(" | L="));
printNum(Serial, l, 0);
Serial.println();
#endif
if (haveSD) {
if (logFile) {
printDateTime(logFile);
logFile.print(';');
printNum(logFile, t, 1);
logFile.print(';');
printNum(logFile, h, 0);
logFile.print(';');
printNum(logFile, p, 1);
logFile.print(';');
printNum(logFile, l, 0);
logFile.println();
logFile.flush();
rotateIfNeed();
gErrors &= ~ERR_SD_RW;
} else {
gErrors |= ERR_SD_RW;
}
} else {
gErrors |= ERR_SD_RW;
}
}
/* ---------- Modes ---------- */
void enterMode(enum Mode m) {
modeCur = m;
switch (m) {
case STANDARD:
baseColor = LED_G;
#if PRETTY_UI
Serial.println(F("🟢 Mode STANDARD — mesure toutes 10 s"));
#else
Serial.println(F("[STD] 10s"));
#endif
break;
case ECONOMIQUE:
baseColor = LED_B;
#if PRETTY_UI
Serial.println(F("🔵 Mode ÉCONOMIQUE — mesure toutes 20 s"));
#else
Serial.println(F("[ECO] 20s"));
#endif
break;
case MAINTENANCE:
baseColor = LED_O;
#if PRETTY_UI
Serial.println(F("🟠 Mode MAINTENANCE — logging désactivé. Tapez READ / EJECT"));
#else
Serial.println(F("[MAINT]"));
#endif
break;
case CONFIGURATION:
baseColor = LED_Y;
#if PRETTY_UI
Serial.println(F("🟡 Mode CONFIGURATION — tapez HELP pour l’aide."));
#else
Serial.println(F("[CFG]"));
#endif
break;
}
setLed(baseColor);
}
unsigned long intStd(void) {
// 10 s
return (unsigned long)cfg.logMin * 1000UL;
}
unsigned long intEco(void) {
// 20 s
return (unsigned long)cfg.logMin * 2000UL;
}
void tickStd(void) {
static unsigned long t = 0;
unsigned long n = millis();
if (t == 0 || n - t >= intStd()) {
t = n;
logOnce();
}
}
void tickEco(void) {
static unsigned long t = 0;
unsigned long n = millis();
if (t == 0 || n - t >= intEco()) {
t = n;
logOnce();
}
}
void tickMaint(void) {
/* silencieux */
}
void tickCfg(void) {
/* silencieux aussi */
}
/* ---------- Aide (config claire) ---------- */
void showHelp(void) {
#if PRETTY_UI
Serial.println(F("\n===== AIDE CONFIG ====="));
Serial.println(F(" LOG_INTERVAL=<sec> ex: LOG_INTERVAL=10"));
Serial.println(F(" TIMEOUT=<s> ex: TIMEOUT=3"));
Serial.println(F(" FILE_MAX_SIZE=<octets> ex: FILE_MAX_SIZE=4096"));
Serial.println(F(" CAPTEUR=BME:on|off ex: CAPTEUR=BME:on"));
Serial.println(F(" CAPTEUR=LUM:on|off ex: CAPTEUR=LUM:off"));
Serial.println(F(" DATE=YYYY-MM-DD HH:MM:SS ex: DATE=2025-10-28 14:20:00"));
Serial.println(F(" READ → mesure immédiate"));
Serial.println(F(" EJECT → sécuriser retrait SD"));
Serial.println(F(" RESET → paramètres par défaut"));
Serial.println(F("=======================\n"));
#else
Serial.println(
F("CMDS: LOG_INTERVAL=<s>, TIMEOUT=<s>, FILE_MAX_SIZE=<o>, CAPTEUR=BME:on|off, CAPTEUR=LUM:on|off, DATE=YYYY-MM-DD HH:MM:SS, READ, EJECT, RESET")
);
#endif
}
/* ---------- Commandes ---------- */
char cmdBuf[96];
uint8_t cmdLen = 0;
void handleCmdChar(char c) {
if (c == '\r') {
return;
}
if (c == '\n') {
handleCmdLine();
cmdLen = 0;
cmdBuf[0]= 0;
return;
}
if (cmdLen < sizeof(cmdBuf) - 1) {
cmdBuf[cmdLen++] = c;
cmdBuf[cmdLen] = 0;
}
}
bool starts(const char *s, const char *p) {
while (*p) {
if (*s++ != *p++) {
return false;
}
}
return true;
}
void handleCmdLine(void) {
char *s = cmdBuf;
while (*s == ' ') {
s++;
}
if (*s == 0) {
return;
}
if (strcmp(s, "HELP") == 0) {
showHelp();
return;
}
if (strcmp(s, "VERSION") == 0) {
Serial.println(F("WWW-Pretty 1.0"));
return;
}
if (starts(s, "LOG_INTERVAL=")) {
cfg.logMin = (uint16_t)atoi(s + 13);
saveEEPROM();
Serial.println(F("OK LOG_INTERVAL"));
return;
}
if (starts(s, "TIMEOUT=")) {
cfg.toutSec = (uint16_t)atoi(s + 8);
saveEEPROM();
Serial.println(F("OK TIMEOUT"));
return;
}
if (starts(s, "FILE_MAX_SIZE=")) {
cfg.maxBytes = (uint32_t)atol(s + 14);
saveEEPROM();
Serial.println(F("OK FILE_MAX_SIZE"));
return;
}
if (starts(s, "CAPTEUR=")) {
char w[4];
char val[4];
if (sscanf(s + 8, "%3[^:]:%3s", w, val) == 2) {
bool on = (val[0] == 'o' || val[0] == 'O');
if (w[0] == 'B' || w[0] == 'b') {
enBME = on;
} else if (w[0] == 'L' || w[0] == 'l') {
enLUM = on;
}
saveEEPROM();
Serial.println(F("OK CAPTEUR"));
}
return;
}
if (starts(s, "DATE=")) {
if (!haveRTC) {
Serial.println(F("ERR RTC"));
return;
}
int y, mo, d, h, mi, se;
if (sscanf(
s + 5,
"%4d-%2d-%2d %2d:%2d:%2d",
&y,
&mo,
&d,
&h,
&mi,
&se
) == 6) {
rtc.adjust(DateTime(y, mo, d, h, mi, se));
Serial.println(F("OK DATE"));
} else {
Serial.println(F("ERR DATE"));
}
return;
}
if (strcmp(s, "READ") == 0) {
logOnce();
return;
}
if (strcmp(s, "EJECT") == 0) {
if (logFile) {
logFile.flush();
logFile.close();
}
Serial.println(F("OK EJECT"));
return;
}
if (strcmp(s, "RESET") == 0) {
setDefaults();
saveEEPROM();
Serial.println(F("OK RESET"));
return;
}
Serial.println(F("Commande inconnue. HELP"));
}
/* ---------- Alertes LED ---------- */
// priorité: SD_RW > SD_FULL > RTC > SENSOR > INCOH
void updateErrorLED() {
if (gErrors == ERR_NONE) {
static LedColor last = LED_OFF;
if (last != baseColor) {
setLed(baseColor);
last = baseColor;
}
return;
}
uint8_t e =
(gErrors & ERR_SD_RW) ? ERR_SD_RW :
(gErrors & ERR_SD_FULL) ? ERR_SD_FULL :
(gErrors & ERR_RTC) ? ERR_RTC :
(gErrors & ERR_SENSOR) ? ERR_SENSOR :
ERR_INCOH;
unsigned long now = millis();
uint16_t tS = 220;
uint16_t tL = 600;
uint16_t tE = 360;
LedColor colA = LED_R;
LedColor colB = LED_W;
uint16_t halfA = tE;
uint16_t halfB = tE;
if (e == ERR_RTC) {
colB = LED_B;
halfA = halfB = tE; // 🔴↔🔵 égal
} else if (e == ERR_SENSOR) {
colB = LED_G;
halfA = halfB = tE; // 🔴↔🟢 égal
} else if (e == ERR_INCOH) {
colB = LED_G;
halfA = tS;
halfB = tL; // 🔴 court / 🟢 long
} else {
colB = LED_W;
halfA = (e == ERR_SD_RW) ? tS : tE;
halfB = (e == ERR_SD_RW) ? tL : tE; // SD: rouge↔blanc
}
static uint8_t lastE = 0xFF;
static uint16_t cur = 0;
if (e != lastE) {
lastE = e;
ledPhase = false;
ledBeatMs = now;
cur = halfA;
setLed(colA);
return;
}
if (now - ledBeatMs >= cur) {
ledBeatMs = now;
ledPhase = !ledPhase;
if (ledPhase) {
setLed(colB);
cur = halfB;
} else {
setLed(colA);
cur = halfA;
}
}
}
/* ---------- Setup / Loop ---------- */
unsigned long lastCmdMs = 0;
const unsigned long CFG_BACK = 30000UL;
void setup() {
Serial.begin(9600);
led.init();
setLed(LED_W);
pinMode(PIN_BTN_V, INPUT_PULLUP);
pinMode(PIN_BTN_R, INPUT_PULLUP);
attachInterrupt(
digitalPinToInterrupt(PIN_BTN_V),
isrV,
CHANGE
);
attachInterrupt(
digitalPinToInterrupt(PIN_BTN_R),
isrR,
CHANGE
);
loadEEPROM();
Wire.begin();
haveRTC = rtc.begin();
if (!haveRTC) {
#if PRETTY_UI
Serial.println(F("⚠️ RTC non détectée"));
#else
Serial.println(F("RTC abs"));
#endif
gErrors |= ERR_RTC;
}
if (SD.begin(PIN_SD_CS)) {
haveSD = true;
openNewLog();
} else {
haveSD = false;
#if PRETTY_UI
Serial.println(F("⚠️ Carte SD absente"));
#else
Serial.println(F("SD abs"));
#endif
gErrors |= ERR_SD_RW;
}
for (uint8_t i = 0; i < sizeof(sInit) / sizeof(sInit[0]); i++) {
if (sInit[i]->initFn) {
sInit[i]->initFn(sInit[i]);
}
}
if (!sBME.present && enBME) {
gErrors |= ERR_SENSOR;
}
if (digitalRead(PIN_BTN_R) == LOW) {
enterMode(CONFIGURATION);
} else {
enterMode(STANDARD);
}
#if PRETTY_UI
Serial.println(F("✦ Aide: tapez HELP (Moniteur série 9600 bauds)"));
#else
Serial.println(F("HELP pr cmds"));
#endif
}
void loop() {
if (fEco) {
fEco = false;
enterMode(ECONOMIQUE);
}
if (fMaint) {
fMaint = false;
modePrev= modeCur;
enterMode(MAINTENANCE);
}
if (fStd) {
fStd = false;
enterMode(STANDARD);
}
if (fQuitM) {
fQuitM = false;
enterMode(modePrev);
}
while (Serial.available()) {
char c = (char)Serial.read();
handleCmdChar(c);
lastCmdMs = millis();
}
switch (modeCur) {
case STANDARD:
tickStd();
break;
case ECONOMIQUE:
tickEco();
break;
case MAINTENANCE:
tickMaint();
break;
case CONFIGURATION:
tickCfg();
break;
}
if (modeCur == CONFIGURATION && (millis() - lastCmdMs > CFG_BACK)) {
enterMode(STANDARD);
}
updateErrorLED();
delay(3);
}
Dans cette première partie, on explique ce qu’on a importé dans le code et pourquoi.
#define PRETTY_UI 1
#include <Wire.h>
Wire.begin() et donc pas lire l’heure.#include <SPI.h>
#include <SD.h>
LOG001.CSV, pour écrire les mesures dedans, et pour vérifier si la carte est présente.#include <EEPROM.h>
#include <RTClib.h>
rtc.begin() pour tester le module, et rtc.now() pour récupérer date + heure.#include <Adafruit_BME280.h>
readTemperature(), readHumidity(), readPressure().#include <ChainableLED.h>
led.setColorRGB(...).Le code comporte quatre modes de fonctionnement, exactement comme le cahier des charges du projet : Standard, Économique, Maintenance et Configuration.
Ces modes sont définis par l’énumération :
enum Mode { STANDARD, ECONOMIQUE, MAINTENANCE, CONFIGURATION };
Chaque mode a ses propres fonctions, son comportement et une couleur LED dédiée.
C’est le mode principal de fonctionnement du système. Il réalise les mesures régulières des capteurs et enregistre les données sur la carte SD.
Quand on est en Standard, la LED est verte continue.
Le programme appelle régulièrement la fonction tickStd().
Cette fonction déclenche logOnce() toutes les 10 secondes (par défaut) pour :
case STANDARD: dans enterMode().tickStd().loop().Ce mode est utilisé pour réduire la consommation d’énergie. Il permet au système de fonctionner plus longtemps sur batterie.
tickEco() appelle logOnce() toutes les 20 secondes (deux fois plus lent que le mode standard).case ECONOMIQUE: dans enterMode().tickEco().isrV() et la lecture du flag fEco dans loop().Le mode Maintenance sert à consulter les données des capteurs sans les enregistrer et à retirer la carte SD en toute sécurité.
READ, EJECT, etc.EJECT ferme le fichier pour éviter toute corruption de données.case MAINTENANCE: dans enterMode().tickMaint() (vide pour désactiver l’écriture).isrR() et le flag fMaint dans loop().Ce mode permet à l’utilisateur de modifier les paramètres du système via la console série (moniteur Arduino). C’est le seul mode où les capteurs ne font aucune mesure.
LED jaune continue.
Le mode Configuration est activé au démarrage si le bouton rouge est maintenu enfoncé, ou depuis le code avec enterMode(CONFIGURATION).
L’acquisition des capteurs est désactivée.
L’utilisateur peut taper des commandes comme :
LOG_INTERVAL=10 → modifier la fréquence des mesures.TIMEOUT=3 → changer le délai avant erreur capteur.FILE_MAX_SIZE=4096 → définir la taille max du fichier.RESET → remettre les valeurs par défaut.DATE=... → régler la date et l’heure du RTC.Si aucune commande n’est tapée pendant 30 secondes, le système retourne automatiquement au mode Standard.
case CONFIGURATION: dans enterMode().handleCmdLine().if (modeCur == CONFIGURATION && (millis() - lastCmdMs > CFG_BACK)) enterMode(STANDARD);.| Mode | Couleur LED | Fonction principale | Méthode d’activation |
|---|---|---|---|
| Standard | 🟢 Vert | Mesure et enregistrement réguliers | Démarrage normal ou retour automatique |
| Économique | 🔵 Bleu | Mesure ralentie, économie d’énergie | Bouton vert (2s) |
| Maintenance | 🟠 Orange | Lecture des capteurs sans écriture | Bouton rouge (2s) |
| Configuration | 🟡 Jaune | Réglage des paramètres via série | Bouton rouge au démarrage |
Dans ce projet, les boutons ne sont pas lus simplement avec digitalRead() dans la boucle. On a choisi d’utiliser des interruptions matérielles. Ça permet au système de réagir tout de suite quand l’utilisateur appuie sur un bouton, même si le programme est en train d’écrire sur la carte SD ou de faire une mesure.
On va expliquer ça en 4 niveaux :
isrV et isrR)Sans interruption, on aurait fait :
if (digitalRead(bouton) == LOW) { ... }
mais ça veut dire qu’on doit tout le temps vérifier le bouton dans la boucle. Si la boucle est lente (écriture SD, calcul…), on peut rater un appui.
Avec les interruptions :
C’est exactement ce qu’on veut pour :
Dans le code, on utilise :
Ces deux broches (2 et 3) de l’Arduino UNO sont justement celles qui supportent les interruptions externes.
Dans le setup(), on voit :
pinMode(PIN_BTN_V, INPUT_PULLUP);
pinMode(PIN_BTN_R, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_BTN_V), isrV, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_BTN_R), isrR, CHANGE);
👉 Ça veut dire :
isrV ou isrR”.CHANGE veut dire : on veut être prévenu à l’appui et au relâchement.On a deux fonctions d’interruption :
void isrV(void) { ... } // bouton vert
void isrR(void) { ... } // bouton rouge
Très important : dans une interruption, on ne fait pas de choses longues (pas de Serial.print, pas d’accès SD). On fait juste le minimum : noter l’heure et poser un drapeau.
L’idée est la même pour les deux boutons :
millis().C’est pour ça qu’on a ces variables globales :
const unsigned long HOLD_MS = 2000; // 2s
volatile unsigned long tV = 0, tR = 0; // temps d’appui vert/rouge
Et dans l’ISR du bouton vert :
tV = millis();millis() - tV >= HOLD_MSDans l’ISR, on ne fait pas enterMode(...). À la place, on met juste un flag :
fEco = true; → “je veux aller en mode éco”fStd = true; → “je veux retourner en mode standard”fMaint = true; → “je veux aller en maintenance”fQuitM = true; → “je veux quitter la maintenance”On utilise volatile bool pour ces flags parce qu’ils sont modifiés en dehors du flux normal (par l’interruption).
C’est la boucle principale (la fonction loop()).
Dans loop(), on trouve :
if (fEco) { fEco = false; enterMode(ECONOMIQUE); }
if (fMaint) { fMaint = false; modePrev = modeCur; enterMode(MAINTENANCE); }
if (fStd) { fStd = false; enterMode(STANDARD); }
if (fQuitM) { fQuitM = false; enterMode(modePrev); }
Donc le fonctionnement complet est :
fEco = true (ou autre)enterMode(...)enterMode(...) change la LED, écrit dans le Serial, met à jour le mode couranttick...() selon le mode👉 C’est une bonne pratique en Arduino :
Le sujet disait :
modePrev)Donc toutes les actions “appui long → changer de mode” sont réalisées grâce aux interruptions.
setDefaults()But : remettre la configuration du système à des valeurs par défaut.
Ce qu’elle fait :
saveEEPROM()But : enregistrer la configuration courante dans la mémoire EEPROM.
Ce qu’elle fait :
EEPROM.put(...)loadEEPROM()But : lire la configuration stockée à l’allumage.
Ce qu’elle fait :
Settings depuis l’EEPROMsetDefaults() et on resauvegardesetup() au démarrage de la carte.crc16(const Settings& s)But : faire un petit contrôle d’intégrité sur la structure de config.
Ce qu’elle fait :
saveEEPROM() et loadEEPROM() pour savoir si ce qu’on a lu est valide.setLed(enum LedColor c)But : allumer la LED avec la bonne couleur.
Ce qu’elle fait : traduit une couleur logique (LED_G, LED_B, LED_Y…) en valeurs RVB pour la bibliothèque ChainableLED.
Utilisée :
enterMode(...) pour afficher la couleur du modeupdateErrorLED() pour afficher les erreursupdateErrorLED()But : afficher les erreurs système avec un code couleur/clignotement.
Ce qu’elle fait :
gErrorsmillis()loop() à chaque tour → permet d’avoir une LED d’erreur indépendante du mode.initBME(struct Sensor* s)But : initialiser le capteur BME280.
Ce qu’elle fait :
setup() pour détecter le matériel présent.readBME(struct Sensor* s, float* t, float* h, float* p)But : lire une mesure du BME.
Ce qu’elle fait :
NANlogOnce() à chaque acquisition.initLUM(struct Sensor* s)But : préparer la lecture de la luminosité sur l’entrée analogique.
Utilisée : dans le setup().
readLUM(struct Sensor* s, float* t, float* h, float* p)But : lire la luminosité.
Ce qu’elle fait : lit analogRead(A0) et met la valeur dans p.
Utilisée : dans logOnce().
printDateTime(Print& out)But : écrire la date et l’heure dans un flux (Serial ou fichier SD).
Ce qu’elle fait :
logOnce() pour le CSV et pour le moniteur série.printNum(Print& out, float v, uint8_t prec)But : afficher une valeur flottante proprement ou “NA”.
Utilisée : dans logOnce().
labelVal(...)But : faire un affichage lisible pour l’utilisateur (Temp=25.4 °C).
Utilisée : dans la version “jolie” de l’affichage série (si PRETTY_UI = 1).
openNewLog()But : créer/ouvrir un nouveau fichier CSV sur la carte SD.
Ce qu’elle fait :
LOGxxx.CSVrotateIfNeed() quand le fichier est pleinrotateIfNeed()But : surveiller la taille du fichier courant et en recréer un nouveau si on dépasse la taille configurée.
Utilisée : à la fin de logOnce().
logOnce()But : c’est la fonction centrale d’acquisition + stockage.
Ce qu’elle fait :
enterMode(enum Mode m)But : basculer le système dans un des 4 modes.
Ce qu’elle fait :
modeCurloop())tickStd()But : comportement du mode Standard.
Ce qu’elle fait : appelle périodiquement logOnce() selon l’intervalle défini.
tickEco()But : comportement du mode Économique.
Ce qu’elle fait : pareil que standard mais deux fois plus lent.
tickMaint()But : comportement du mode Maintenance.
Ce qu’elle fait : ne fait rien (pas de log), laisse la main à l’utilisateur.
tickCfg()But : comportement du mode Configuration.
Ce qu’elle fait : rien en boucle → on attend les commandes série.
handleCmdChar(char c)But : recevoir les caractères un par un depuis le port série.
Ce qu’elle fait :
\n → on appelle handleCmdLine()loop() à chaque fois qu’il y a Serial.available().handleCmdLine()But : analyser la ligne tapée par l’utilisateur et exécuter la commande.
Ce qu’elle fait :
HELP, VERSION, LOG_INTERVAL=, TIMEOUT=, FILE_MAX_SIZE=, CAPTEUR=, DATE=, READ, EJECT, RESETshowHelp()But : afficher la liste des commandes disponibles.
Utilisée : quand l’utilisateur tape HELP.
isrV() (bouton vert)But : détecter un appui long sur le bouton vert.
Ce qu’elle fait :
attachInterrupt(...).isrR() (bouton rouge)But : détecter un appui long sur le bouton rouge.
Ce qu’elle fait :
setup()But : initialiser tout le système au démarrage.
Ce qu’elle fait :
loop()But : faire tourner le système en continu.
Ce qu’elle fait :
tickStd, tickEco, …)Notre équipe a réalisé une station météo embarquée complète et autonome à base d’Arduino UNO, capable de mesurer, enregistrer et signaler les données météorologiques de manière fiable. Le système gère quatre modes de fonctionnement, une LED d’état, une interface série pour la configuration et une mémoire EEPROM pour sauvegarder les paramètres. Ce projet nous a permis de comprendre concrètement le fonctionnement d’un système embarqué, de maîtriser les capteurs et la gestion de données, tout en respectant les exigences techniques du programme Worldwide Weather Watcher.